﻿//
// (C) Copyright 2009 Irantha Suwandarathna (iranthas@hotmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#if !SILVERLIGHT
using System.Data.Common;
using System.Data;
#endif
using EffiProz.Core.Errors;
using EffiProz.Core.Lib;

namespace EffiProz.Core.DataTypes
{
    public class BinaryType : SqlType
    {
        const long maxBinaryPrecision = int.MaxValue;

        public BinaryType(int type, long precision)
            : base(Types.SQL_VARBINARY, type, precision, 0)
        {

        }

        public override int displaySize()
        {
            return precision > int.MaxValue ? int.MaxValue
                                                 : (int)precision;
        }

        public override int getAdoTypeCode()
        {
#if !SILVERLIGHT
            return (int)DbType.Binary;
#else
            return -1;
#endif
        }

        public override String getCSharpClassName()
        {
            return "System.Byte[]";
        }

        public override String getNameString()
        {
            return typeCode == Types.SQL_BINARY ? Tokens.T_BINARY
                                                : Tokens.T_VARBINARY;
        }

        public String getNameFullString()
        {
            return typeCode == Types.SQL_BINARY ? Tokens.T_BINARY
                                                : "BINARY VARYING";
        }

        public override String getDefinition()
        {

            if (precision == 0)
            {
                return getNameString();
            }

            StringBuilder sb = new StringBuilder(16);

            sb.Append(getNameString());
            sb.Append('(');
            sb.Append(precision);
            sb.Append(')');

            return sb.ToString();
        }

        public override bool isBinaryType()
        {
            return true;
        }

        public override bool acceptsPrecision()
        {
            return true;
        }

        public override long getMaxPrecision()
        {
            return maxBinaryPrecision;
        }

        public override bool requiresPrecision()
        {
            return typeCode == Types.SQL_VARBINARY;
        }

        /**
         * relaxes the SQL standard list to avoid problems with covnersion of
         * literals and java method parameter type issues
         */
        public override int precedenceDegree(SqlType other)
        {

            if (other.typeCode == typeCode)
            {
                return 0;
            }

            if (!other.isBinaryType())
            {
                return int.MinValue;
            }

            switch (typeCode)
            {

                case Types.SQL_BIT:
                case Types.SQL_BIT_VARYING:
                    return int.MinValue;

                case Types.SQL_BINARY:
                    return other.typeCode == Types.SQL_BLOB ? 4
                                                            : 2;

                case Types.SQL_VARBINARY:
                    return other.typeCode == Types.SQL_BLOB ? 4
                                                            : 2;

                case Types.SQL_BLOB:
                    return other.typeCode == Types.SQL_BINARY ? -4
                                                              : -2;

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "BinaryType");
            }
        }

        public override SqlType getAggregateType(SqlType other)
        {

            if (typeCode == other.typeCode)
            {
                return precision >= other.precision ? this
                                                    : other;
            }

            if (other.isCharacterType())
            {
                return other.getAggregateType(this);
            }

            switch (other.typeCode)
            {

                case Types.SQL_ALL_TYPES:
                    return this;

                case Types.SQL_BIT:
                case Types.SQL_BIT_VARYING:
                    {
                        long bytePrecision = (other.precision + 7) / 8;

                        return precision >= bytePrecision ? this
                                                          : getBinaryType(
                                                              this.typeCode,
                                                              bytePrecision);
                    }
                case Types.SQL_BINARY:
                    return precision >= other.precision ? this
                                                        : getBinaryType(typeCode,
                                                        other.precision);

                case Types.SQL_VARBINARY:
                    if (typeCode == Types.SQL_BLOB)
                    {
                        return precision >= other.precision ? this
                                                            : getBinaryType(
                                                            typeCode,
                                                            other.precision);
                    }
                    else
                    {
                        return other.precision >= precision ? other
                                                            : getBinaryType(
                                                            other.typeCode,
                                                            precision);
                    }
                case Types.SQL_BLOB:
                    return other.precision >= precision ? other
                                                        : getBinaryType(
                                                        other.typeCode, precision);

                default:
                    throw Error.error(ErrorCode.X_42562);
            }
        }

        /**
         * Returns type for concat
         */
        public override SqlType getCombinedType(SqlType other, int operation)
        {

            if (operation != OpTypes.CONCAT)
            {
                return getAggregateType(other);
            }

            SqlType newType;
            long newPrecision = this.precision + other.precision;

            switch (other.typeCode)
            {

                case Types.SQL_ALL_TYPES:
                    return this;

                case Types.SQL_BIT:
                case Types.SQL_BIT_VARYING:
                    newPrecision = this.precision + (other.precision + 7) / 8;
                    newType = this;
                    break;

                case Types.SQL_BINARY:
                    newType = this;
                    break;

                case Types.SQL_VARBINARY:
                    newType = (typeCode == Types.SQL_BLOB) ? this
                                                           : other;
                    break;

                case Types.SQL_BLOB:
                    newType = other;
                    break;

                default:
                    throw Error.error(ErrorCode.X_42561);
            }

            if (newPrecision > maxBinaryPrecision)
            {
                if (typeCode == Types.SQL_BINARY)
                {

                    // Standard disallows type length reduction
                    throw Error.error(ErrorCode.X_42570);
                }
                else if (typeCode == Types.SQL_VARBINARY)
                {
                    newPrecision = maxBinaryPrecision;
                }
            }

            return getBinaryType(newType.typeCode, newPrecision);
        }

        public override int compare(Session session, Object a, Object b)
        {

            if (a == b)
            {
                return 0;
            }

            if (a == null)
            {
                return -1;
            }

            if (b == null)
            {
                return 1;
            }

            if (a is BinaryData && b is BinaryData)
            {
                byte[] data1 = ((BinaryData)a).getBytes();
                byte[] data2 = ((BinaryData)b).getBytes();
                int length = data1.Length > data2.Length ? data2.Length
                                                            : data1.Length;

                for (int i = 0; i < length; i++)
                {
                    if (data1[i] == data2[i])
                    {
                        continue;
                    }

                    return (((int)data1[i]) & 0xff) > (((int)data2[i]) & 0xff)
                           ? 1
                           : -1;
                }

                if (data1.Length == data2.Length)
                {
                    return 0;
                }

                return data1.Length > data2.Length ? 1
                                                   : -1;
            }

            throw Error.runtimeError(ErrorCode.U_S0500, "BinaryType");
        }

        public override Object convertToTypeLimits(SessionInterface session, Object a)
        {
            return castOrConvertToType(session, a, this, false);
        }

        public override Object castToType(SessionInterface session, Object a,
                                 SqlType otherType)
        {
            return castOrConvertToType(session, a, otherType, true);
        }

        public override Object convertToType(SessionInterface session, Object a,
                                    SqlType otherType)
        {
            return castOrConvertToType(session, a, otherType, false);
        }

        public override Object convertCSharpToSQL(SessionInterface session, Object a)
        {

            if (a is byte[])
            {
                return new BinaryData((byte[])a, true);
            }

            throw Error.error(ErrorCode.X_42561);
        }

        public override Object convertSQLToCSharp(SessionInterface session, Object a)
        {
            return ((BlobData)a).getBytes();
        }

        Object castOrConvertToType(SessionInterface session, Object a,
                                   SqlType otherType, bool cast)
        {

            BlobData b;

            if (a == null)
            {
                return null;
            }

            switch (otherType.typeCode)
            {

                // non-SQL feature, for compatibility with previous versions
                case Types.SQL_VARCHAR:
                case Types.SQL_CHAR:
                    {
                        b = session.getScanner().convertToBinary((String)a);
                        otherType = getBinaryType(Types.SQL_VARBINARY,
                                                  b.length(session));

                        break;
                    }
                case Types.SQL_BIT:
                    {
                        b = (BlobData)a;
                        otherType = getBinaryType(Types.SQL_VARBINARY,
                                                  b.length(session));

                        break;
                    }
                case Types.SQL_BINARY:
                case Types.SQL_VARBINARY:
                case Types.SQL_BLOB:
                    b = (BlobData)a;
                    break;

                default:
                    throw Error.error(ErrorCode.X_22501);
            }

            if (precision == 0)
            {
                return b;    // never a blob
            }

            if (b.length(session) > precision
                    && b.nonZeroLength(session) > precision)
            {
                if (!cast)
                {
                    throw Error.error(ErrorCode.X_22001);
                }

                session.addWarning(Error.error(ErrorCode.W_01004));
            }

            if (otherType.typeCode == Types.SQL_BLOB)
            {
                byte[] bytes = b.getBytes(session, 0, (int)precision);

                b = new BinaryData(bytes, false);
            }

            switch (typeCode)
            {

                case Types.SQL_BINARY:
                    {
                        if (b.length(session) > precision)
                        {
                            byte[] data = b.getBytes(session, 0, (int)precision);

                            b = new BinaryData(data, false);
                        }
                        else if (b.length(session) < precision)
                        {
                            byte[] data = (byte[])ArrayUtil.resizeArray(b.getBytes(),
                                (int)precision);

                            b = new BinaryData(data, false);
                        }

                        return b;
                    }
                case Types.SQL_VARBINARY:
                    {
                        if (b.length(session) > precision)
                        {
                            byte[] data = b.getBytes(session, 0, (int)precision);

                            b = new BinaryData(data, false);
                        }

                        return b;
                    }
                default:
                    break;
            }

            throw Error.error(ErrorCode.X_22501);
        }

        public override Object convertToDefaultType(SessionInterface session, Object a)
        {

            if (a == null)
            {
                return a;
            }

            if (a is byte[])
            {
                return new BinaryData((byte[])a, false);
            }
            else if (a is BinaryData)
            {
                return a;
            }
            else if (a is String)
            {
                return castOrConvertToType(session, a, SqlType.SQL_VARCHAR, false);
            }

            throw Error.error(ErrorCode.X_22501);
        }

        public override String convertToString(Object a)
        {

            if (a == null)
            {
                return null;
            }

            return StringConverter.byteArrayToHexString(((BlobData)a).getBytes());
        }

        public override String convertToSQLString(Object a)
        {

            if (a == null)
            {
                return Tokens.T_NULL;
            }

            return StringConverter.byteArrayToSQLHexString(
                ((BinaryData)a).getBytes());
        }

        public override bool canConvertFrom(SqlType otherType)
        {
            return otherType.typeCode == Types.SQL_ALL_TYPES
                   || otherType.isBinaryType() || otherType.isCharacterType();
        }

        public long position(SessionInterface session, BlobData data,
                             BlobData otherData, SqlType otherType, long offset)
        {

            if (data == null || otherData == null)
            {
                return -1L;
            }

            long otherLength = ((BlobData)data).length(session);

            if (offset + otherLength > data.length(session))
            {
                return -1;
            }

            return data.position(session, otherData, offset);
        }

        public virtual BlobData substring(SessionInterface session, BlobData data,
                                  long offset, long length, bool hasLength)
        {

            long end;
            long dataLength = data.length(session);

            if (hasLength)
            {
                end = offset + length;
            }
            else
            {
                end = dataLength > offset ? dataLength
                                          : offset;
            }

            if (offset > end)
            {
                throw Error.error(ErrorCode.X_22011);
            }

            if (offset > end || end < 0)
            {

                // return zero length data
                offset = 0;
                end = 0;
            }

            if (offset < 0)
            {
                offset = 0;
            }

            if (end > dataLength)
            {
                end = dataLength;
            }

            length = end - offset;

            // change method signature to take long
            byte[] bytes = ((BlobData)data).getBytes(session, offset,
                (int)length);

            return new BinaryData(bytes, false);
        }

        int getRightTrimSize(BlobData data)
        {

            byte[] bytes = data.getBytes();
            int endindex = bytes.Length;

            for (--endindex; endindex >= 0 && bytes[endindex] == 0; endindex--) { }

            return ++endindex;
        }

        public BlobData trim(Session session, BlobData data, int trim,
                             bool leading, bool trailing)
        {

            if (data == null)
            {
                return null;
            }

            byte[] bytes = ((BlobData)data).getBytes();
            int endindex = bytes.Length;

            if (trailing)
            {
                for (--endindex; endindex >= 0 && bytes[endindex] == trim;
                        endindex--) { }

                endindex++;
            }

            int startindex = 0;

            if (leading)
            {
                while (startindex < endindex && bytes[startindex] == trim)
                {
                    startindex++;
                }
            }

            byte[] newBytes = bytes;

            if (startindex != 0 || endindex != bytes.Length)
            {
                newBytes = new byte[endindex - startindex];

                Array.Copy(bytes, startindex, newBytes, 0,
                                 endindex - startindex);
            }

            if (typeCode == Types.SQL_BLOB)
            {
                BlobData blob = session.createBlob(newBytes.Length);

                blob.setBytes(session, 0, newBytes);

                return blob;
            }
            else
            {
                return new BinaryData(newBytes, newBytes == bytes);
            }
        }

        public virtual BlobData overlay(Session session, BlobData data, BlobData overlay,
                                long offset, long length, bool hasLength)
        {

            if (data == null || overlay == null)
            {
                return null;
            }

            if (!hasLength)
            {
                length = ((BlobData)overlay).length(session);
            }

            switch (typeCode)
            {

                case Types.SQL_BINARY:
                case Types.SQL_VARBINARY:
                    {
                        BinaryData binary =
                            new BinaryData(session,
                                           substring(session, data, 0, offset, true),
                                           overlay);

                        binary = new BinaryData(session, binary,
                                                substring(session, data,
                                                          offset + length, 0, false));

                        return binary;
                    }
                case Types.SQL_BLOB:
                    {
                        byte[] bytes = substring(session, data, 0, offset,
                                                 false).getBytes();
                        long blobLength = data.length(session)
                                          + overlay.length(session) - length;
                        BlobData blob = session.createBlob(blobLength);

                        blob.setBytes(session, 0, bytes);
                        blob.setBytes(session, blob.length(session),
                                      overlay.getBytes());

                        bytes = substring(session, data, offset + length, 0,
                                          false).getBytes();

                        blob.setBytes(session, blob.length(session), bytes);

                        return blob;
                    }
                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "BinaryType");
            }
        }

        public override Object concat(Session session, Object a, Object b)
        {

            if (a == null || b == null)
            {
                return null;
            }

            long length = ((BlobData)a).length(session)
                          + ((BlobData)b).length(session);

            if (length > precision)
            {
                throw Error.error(ErrorCode.X_22001);
            }

            if (typeCode == Types.SQL_BLOB)
            {
                BlobData blob = session.createBlob(length);

                blob.setBytes(session, 0, ((BlobData)b).getBytes());
                blob.setBytes(session, ((BlobData)a).length(session),
                              ((BlobData)b).getBytes());

                return blob;
            }
            else
            {
                return new BinaryData(session, (BlobData)a, (BlobData)b);
            }
        }

        /** @todo check and adjust max precision */
        public static BinaryType getBinaryType(int type, long precision)
        {

            switch (type)
            {

                case Types.SQL_BINARY:
                case Types.SQL_VARBINARY:
                    return new BinaryType(type, precision);

                case Types.SQL_BLOB:
                    return new BlobType(precision);

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "BinaryType");
            }
        }
    }
}
